﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Xml;
using Kennedy.ManagedHooks;
using LockCrypt.AutoType;
using LockCrypt.Core.AutoType;
using System.Threading;
using LockCrypt.Core;
using System.Text.RegularExpressions;

namespace LockCrypt {
    /// <summary>
    /// Records and plays back keystrokes and mouse clicks.
    /// </summary>
    public class AutoTypeRecorder : DependencyObject {
        private static readonly KeyboardTracking _keyboard = new KeyboardTracking();
        private static readonly MouseHook _mouse = new MouseHook();
        private StringBuilder _recordedAction;
        private StringBuilder _keyBuffer = new StringBuilder();
        private static readonly ILogProvider _logger = new ConsoleLogger();

        public static RoutedUICommand Record = new RoutedUICommand("Record", "Record", typeof(LockCryptCommands));

        public static readonly DependencyProperty IsRecordingProperty = DependencyProperty.Register("IsRecording", typeof(bool), typeof(WinAddAccount), new PropertyMetadata(false));
        /// <summary>
        /// Gets or sets whether a recording is in progress.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if a recording is in progress, otherwise <c>false</c>.
        /// </value>
        public bool IsRecording {
            get { return Convert.ToBoolean(base.GetValue(IsRecordingProperty)); }
            set { base.SetValue(IsRecordingProperty, value); }
        }

        /// <summary>
        /// Gets the recorded which was recorded.
        /// </summary>
        /// <value>The recorded action.</value>
        public string RecordedAction {
            get { return _recordedAction.ToString(); }
        }

        public AutoTypeRecorder()
            : base() {
            _recordedAction = new StringBuilder();
        }

        /// <summary>
        /// Gets the names of all currently open windows.
        /// </summary>
        public static List<string> GetWindows() {
            WindowEnumerator.EnumWindows windows = new WindowEnumerator.EnumWindows();
            windows.GetWindows();
            List<string> windowNames = new List<string>();
            foreach(WindowEnumerator.EnumWindowsItem window in windows.Items) {
                if(window.Visible && window.Text.Length > 0 && window.ClassName != "Button") {
                    windowNames.Add(window.Text);
                }
            }
            return windowNames;
        }

        /// <summary>
        /// Starts recording keystrokes.
        /// </summary>
        public void RecordAction() {
            IsRecording = true;
            EnableRecording();
            _recordedAction = new StringBuilder();
        }

        /// <summary>
        /// Stop recording keystrokes.
        /// </summary>
        public void StopRecording() {
            IsRecording = false;
        }

        /// <summary>
        /// Records mouse events.
        /// </summary>
        /// <param name="mEvent">The mouse event.</param>
        /// <param name="point">The point.</param>
        private void mouse_MouseEvent(MouseEvents mEvent, System.Drawing.Point point) {
            if(IsRecording) {
                WriteBuffer();
                string actionName = null;
                switch(mEvent) {
                    case MouseEvents.LeftButtonDown:
                        actionName = "MouseDown:L";
                        break;
                    case MouseEvents.LeftButtonUp:
                        actionName = "MouseUp:L";
                        break;
                    case MouseEvents.RightButtonDown:
                        actionName = "MouseDown:R";
                        break;
                    case MouseEvents.RightButtonUp:
                        actionName = "MouseUp:R";
                        break;
                }
                if(!string.IsNullOrEmpty(actionName)) {
                    _recordedAction.AppendFormat("{{{0},{1},{2}}}\n", actionName, point.X, point.Y);
                }
            }
        }

        /// <summary>
        /// Records a keystroke.
        /// </summary>
        /// <param name="kEvent">The key event.</param>
        /// <param name="key">The key.</param>
        private void keyboard_KeyboardEvent(KeyboardEvents kEvent, Keys key) {
            if(IsRecording) {
                _logger.Log(string.Format("Key: {0}", key.ToString()));
                switch(key) {
                    case Keys.Shift:
                    case Keys.ShiftKey:
                    case Keys.Control:
                    case Keys.ControlKey:
                    case Keys.Alt:
                        return;
                }
                int keyCode = (int)key;
                StringBuilder valueToRecord = new StringBuilder();
                if((Control.ModifierKeys & Keys.Control) != 0) { valueToRecord.Append('^'); }
                if((Control.ModifierKeys & Keys.Shift) != 0) { valueToRecord.Append('+'); }
                if((Control.ModifierKeys & Keys.Alt) != 0) { valueToRecord.Append('%'); }
                if(keyCode >= 65 && keyCode <= 90) { // a-z
                    // it's a key in a sequence, record it to the buffer this time
                    _keyBuffer.Append(valueToRecord.ToString() + key.ToString().ToLowerInvariant());
                } else {
                    WriteBuffer();
                    if(keyCode >= 112 && keyCode <= 135) { // F1 - F24
                        valueToRecord.Append("{" + key.ToString().ToUpper() + "}");
                    } else if(keyCode >= 96 && keyCode <= 105) { // numpad 0-9
                        valueToRecord.Append(keyCode - 96);
                    } else if(keyCode >= 48 && keyCode <= 57) { // 0-9
                        valueToRecord.Append(key.ToString()[1]);
                    } else {
                        switch(key) {
                            case Keys.Add:
                            case Keys.Subtract:
                            case Keys.Multiply:
                            case Keys.Divide:
                            case Keys.NumLock:
                            case Keys.Up:
                            case Keys.Down:
                            case Keys.Left:
                            case Keys.Right:
                            case Keys.Home:
                            case Keys.End:
                            case Keys.Insert:
                            case Keys.Delete:
                            case Keys.Tab:
                                valueToRecord.Append("{" + key.ToString().ToUpper() + "}");
                                break;
                            case Keys.Scroll:
                                valueToRecord.Append("{SCROLLLOCK}");
                                break;
                            case Keys.Back:
                                valueToRecord.Append("{BACKSPACE}");
                                break;
                            case Keys.Escape:
                                valueToRecord.Append("{ESC}");
                                break;
                            case Keys.CapsLock: // also Capital
                                valueToRecord.Append("{CAPSLOCK}");
                                break;
                            case Keys.Enter: // also Return
                                valueToRecord.Append("{ENTER}");
                                break;
                            case Keys.Space:
                                _keyBuffer.Append(" ");
                                break;
                            default:
                                _logger.Log(string.Format("Unsupported key: {0}", key.ToString()));
                                break;
                        }
                    }
                    if(valueToRecord.Length > 0) {
                        _recordedAction.AppendFormat("{{Keys:{0}}}\n", valueToRecord);
                    }
                }
            }
        }

        /// <summary>
        /// Dumps the key buffer if it is not empty.
        /// </summary>
        private void WriteBuffer() {
            if(_keyBuffer.Length > 0) { // keys in the buffer, write them and reset the buffer
                _recordedAction.AppendFormat("{{Keys:{0}}}\n", _keyBuffer.ToString());
                _keyBuffer = new StringBuilder();
            }
        }

        /// <summary>
        /// Installs keyboard and mouse hooks to enable recording.
        /// </summary>
        public void EnableRecording() {
            if(!_keyboard.IsHooked) {
                _keyboard.InstallHook();
                _keyboard.FilterMessage(KeyboardEvents.KeyUp);
                _keyboard.FilterMessage(KeyboardEvents.SystemKeyUp);
                _keyboard.KeyboardEvent += new KeyboardHook.KeyboardEventHandler(keyboard_KeyboardEvent);
            }
            if(!_mouse.IsHooked) {
                _mouse.InstallHook();
                _mouse.FilterMessage(MouseEvents.Move);
                _mouse.MouseEvent += new MouseHook.MouseEventHandler(mouse_MouseEvent);
            }
        }

        /// <summary>
        /// Uninstalls keyboard and mouse hooks, disabling recording.
        /// </summary>
        public void DisableRecording() {
            _keyboard.UninstallHook();
            _keyboard.UninstallHook();
            _mouse.UninstallHook();
        }

        /// <summary>
        /// Plays back a set of actions.
        /// </summary>
        /// <param name="owner">The owner window.</param>
        /// <param name="config">The configuration to playback.</param>
        /// <param name="fields">The account fileds to be used when substituting.</param>
        public static void Playback(Window owner, AutoTypeConfiguration config, IEnumerable<IField> fields) {
            WindowEnumerator.EnumWindows windows = new WindowEnumerator.EnumWindows();
            windows.GetWindows();
            bool matchExact = false,
                 matchStart = false,
                 matchEnd = false,
                 matchContains = false;
            string windowToFind = null;
            int firstWildcardPosition = config.TargetWindow.IndexOf('*');
            if(firstWildcardPosition >= 0) {
                if(config.TargetWindow[0] == '*') {
                    if(config.TargetWindow.Length < 2) // at least *x
                        throw new ArgumentException("Invalid target window name");
                    if(config.TargetWindow[config.TargetWindow.Length - 1] == '*') {
                        if(config.TargetWindow.Length < 3) // at least *x*
                            throw new ArgumentException("Invalid target window name");
                        matchContains = true;
                        windowToFind = config.TargetWindow.Substring(1, config.TargetWindow.Length - 2);
                    } else {
                        matchEnd = true;
                        windowToFind = config.TargetWindow.Substring(1, config.TargetWindow.Length - 1);
                    }
                } else if(config.TargetWindow[config.TargetWindow.Length - 1] == '*') {
                    matchStart = true;
                    windowToFind = config.TargetWindow.Substring(0, config.TargetWindow.Length - 1);
                }
            }
            if(!matchStart && !matchEnd && !matchContains) {
                matchExact = true;
                windowToFind = config.TargetWindow;
            }

            List<WindowEnumerator.EnumWindowsItem> possibleWindows = new List<WindowEnumerator.EnumWindowsItem>();
            foreach(WindowEnumerator.EnumWindowsItem window in windows.Items) {
                if(window.Visible && window.Text.Length > 0 && window.ClassName != "Button") {
                    if(matchExact && window.Text == windowToFind) {
                        possibleWindows.Add(window);
                    } else if(matchStart && window.Text.StartsWith(windowToFind)) {
                        possibleWindows.Add(window);
                    } else if(matchEnd && window.Text.EndsWith(windowToFind)) {
                        possibleWindows.Add(window);
                    } else if(matchContains && window.Text.Contains(windowToFind)) {
                        possibleWindows.Add(window);
                    }
                }
            }

            if(possibleWindows.Count == 0) {
                throw new Exception(string.Format("Could not find window '{0}'", config.TargetWindow));
            } else if(possibleWindows.Count == 1) {
                possibleWindows[0].Restore();
            } else {
                WinChooseWindow dlg = new WinChooseWindow(possibleWindows.Select(w => w.Text)) { Owner = owner };
                if(dlg.ShowDialog() != true) {
                    return;
                }
                var window = possibleWindows.FirstOrDefault(w => w.Text == dlg.SelectedWindow);
                if(window == null)
                    throw new Exception("Could not find window");
                window.Restore();
            }

            BackgroundWorker worker = new BackgroundWorker();
            worker.DoWork += (s, e) => {
                var fieldDictionary = fields.ToDictionary(f => f.Name, f => f.Value);
                Thread.Sleep(500);
                foreach(AutoTypeCommand command in config.Commands) {
                    try {
                        if(command is MouseCommand) {
                            MouseCommand mouseCmd = command as MouseCommand;
                            switch(command.Action) {
                                case CommandType.MouseClick:
                                    MouseSimulator.Position = new System.Drawing.Point(mouseCmd.X, mouseCmd.Y);
                                    MouseSimulator.Click(mouseCmd.Button);
                                    Thread.Sleep(50);
                                    break;
                                case CommandType.MouseDown:
                                    MouseSimulator.Position = new System.Drawing.Point(mouseCmd.X, mouseCmd.Y);
                                    MouseSimulator.MouseDown(mouseCmd.Button);
                                    Thread.Sleep(50);
                                    break;
                                case CommandType.MouseUp:
                                    MouseSimulator.Position = new System.Drawing.Point(mouseCmd.X, mouseCmd.Y);
                                    MouseSimulator.MouseUp(mouseCmd.Button);
                                    Thread.Sleep(50);
                                    break;
                            }
                        } else {
                            switch(command.Action) {
                                case CommandType.Keys:
                                    StringBuilder keys = new StringBuilder((command as KeyCommand).Keys);
                                    foreach(IField field in fields) {
                                        keys.Replace(string.Format("${0}$", field.Name), field.Value);
                                    }
                                    /*MatchCollection fieldVariables = Regex.Matches(keys, @"\$.*?\$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
                                    foreach(Match fieldMatch in fieldVariables) {
                                        if(!fieldMatch.Success)
                                            continue;
                                        string fieldNameToMatch = fieldMatch.Value;
                                        if(fieldDictionary.ContainsKey(fieldNameToMatch)) {
                                            keys.Replace(
                                        }
                                    }*/
                                    SendKeys.SendWait(keys.ToString());
                                    _logger.Log(string.Format("Sent keys: {0}", keys));
                                    Thread.Sleep(25);
                                    break;

                                case CommandType.Wait:
                                    int delay = (command as WaitCommand).Delay;
                                    _logger.Log(string.Format("Waiting {0}ms", delay));
                                    Thread.Sleep(delay);
                                    break;
                            }
                        }
                    } catch(Exception ex) {
                        _logger.Log(string.Format("Error sending command: {0}", ex.Message));
                    }

                }
            };
            worker.RunWorkerAsync();
        }
    }
}